﻿// -----------------------------------------------------------------------
// Copyright (c) Microsoft Corporation.  All rights reserved.
// -----------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Microsoft.Internal;
using System.Threading;
using System.Globalization;
using System.Diagnostics;

namespace System.ComponentModel.Composition
{
    public class ComposablePartCatalogExportProvider : ExportProvider, IDisposable
    {
        private Lock _lock = new Lock();
        private Dictionary<ComposablePartDefinition, ComposablePart> _activatedSingletons = new Dictionary<ComposablePartDefinition, ComposablePart>();
        private ComposablePartCatalog _catalog;
        private EventHandler<ExportsChangedEventArgs> _changedEventStorage;
        private volatile bool _isDisposed = false;
        private volatile bool _isRunning = false;
        private ExportProvider _sourceProvider;
        private CompositionEngine _compositionEngine;


        /// <summary>
        /// Initializes a new instance of the <see cref="ComposablePartCatalogExportProvider"/> class.
        /// </summary>
        /// <param name="catalog">The catalog.</param>
        public ComposablePartCatalogExportProvider(ComposablePartCatalog catalog)
        {
            Requires.NotNull(catalog, "catalog");

            this._catalog = catalog;

            var notifyCatalogChanged = this._catalog as INotifyComposablePartCatalogChanged;
            if (notifyCatalogChanged != null)
            {
                notifyCatalogChanged.Changed += this.OnCatalogChanged;
            }
        }

        /// <summary>
        /// Releases unmanaged and - optionally - managed resources
        /// </summary>
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Releases unmanaged and - optionally - managed resources
        /// </summary>
        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (!this._isDisposed)
                {
                    bool disposeLock = false;
                    INotifyComposablePartCatalogChanged catalogToUnsubscribeFrom = null;
                    try
                    {
                        using (new WriteLock(this._lock))
                        {
                            if (!this._isDisposed)
                            {
                                catalogToUnsubscribeFrom = this._catalog as INotifyComposablePartCatalogChanged;
                                this._catalog = null;
                                // TODO : this is where disposal of things should really occur
                                this._activatedSingletons = null;
                                disposeLock = true;
                                this._isDisposed = true;
                            }
                        }
                    }
                    finally
                    {
                        if (catalogToUnsubscribeFrom != null)
                        {
                            catalogToUnsubscribeFrom.Changed -= this.OnCatalogChanged;
                        }

                        if (disposeLock)
                        {
                            this._lock.Dispose();
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Gets or sets the source provider.
        /// </summary>
        /// <value>The source provider.</value>
        public ExportProvider SourceProvider
        {
            get
            {
                this.ThrowIfDisposed();

                return this._sourceProvider;
            }
            set
            {
                this.ThrowIfDisposed();

                Requires.NotNull(value, "value");
                using (new WriteLock(this._lock))
                {
                    this.EnsureCanSet(this._sourceProvider);
                    this._sourceProvider = value;
                }

                // This should be safe to do outside the lock, because only the first setter will ever win
                // and others will throw
                CompositionEngine compositionEngine = new CompositionEngine();
                compositionEngine.SourceProvider = _sourceProvider;
                Thread.MemoryBarrier();
                this._compositionEngine = compositionEngine;
            }
        }

        /// <summary>
        /// Returns all exports that match the conditions of the specified import.
        /// </summary>
        /// <param name="importDefinition">The <see cref="ImportDefinition"/> that defines the conditions of the
        /// <see cref="Export"/> to get.</param>
        /// <returns></returns>
        /// <result>
        /// An <see cref="IEnumerable{T}"/> of <see cref="Export"/> objects that match
        /// the conditions defined by <see cref="ImportDefinition"/>, if found; otherwise, an
        /// empty <see cref="IEnumerable{T}"/>.
        /// </result>
        /// <remarks>
        /// 	<note type="inheritinfo">
        /// The implementers should not treat the cardinality-related mismatches as errors, and are not
        /// expected to throw exceptions in those cases.
        /// For instance, if the import requests exactly one export and the provider has no matching exports or more than one,
        /// it should return an empty <see cref="IEnumerable{T}"/> of <see cref="Export"/>.
        /// </note>
        /// </remarks>
        protected override IEnumerable<Export> GetExportsCore(ImportDefinition importDefinition)
        {
            Requires.NotNull(importDefinition, "import");
            this.ThrowIfDisposed();
            this.EnsureRunning();

            List<Export> exports = new List<Export>();
            foreach (Tuple<ComposablePartDefinition, ExportDefinition> partDefinitionAndExportDefinition in this._catalog.GetExports(importDefinition.Constraint))
            {
                ComposablePartDefinition partDefinition = partDefinitionAndExportDefinition.First;
                ExportDefinition exportDefinition = partDefinitionAndExportDefinition.Second;

                if (partDefinition.Metadata.GetValue<bool>(MetadataServices.StandardKeys.IsFactoryKey) == true)
                {
                    exports.Add(this.CreateExportForFactory(partDefinition, exportDefinition));
                }
                else
                {
                    exports.Add(this.CreateExportForSingleton(partDefinition, exportDefinition));
                }
            }

            return exports;
        }

        /// <summary>
        ///     Occurs when the exports in the <see cref="ExportProvider"/> change.
        /// </summary>
        /// <remarks>
        ///     The <see cref="ExportsChanged" /> event passes an <see cref="ExportsChangedEventArgs" />
        ///     which provides a list of contract names for the exports that have changed. An export
        ///     is considered to have changed if it has been added, removed, or updated in the 
        ///     <see cref="ComposablePartCatalogExportProvider" />.
        /// </remarks>
        public override event EventHandler<ExportsChangedEventArgs> ExportsChanged
        {
            add
            {
                using (new WriteLock(this._lock))
                {
                    this._changedEventStorage += value;
                }
            }
            remove
            {
                using (new WriteLock(this._lock))
                {
                    this._changedEventStorage -= value;
                }
            }
        }

        /// <summary>
        /// Raises the <see cref="AggregatingExportProvider.ExportsChanged" /> event.
        /// </summary>
        /// <param name="e">The <see cref="System.ComponentModel.Composition.ExportsChangedEventArgs"/> instance containing the event data.</param>
        protected override void OnExportsChanged(ExportsChangedEventArgs e)
        {
            EventHandler<ExportsChangedEventArgs> changedEvent = null;

            using (new ReadLock(this._lock))
            {
                changedEvent = this._changedEventStorage;
            }

            if (changedEvent != null)
            {
                CompositionResult result = CompositionServices.TryFire(changedEvent, this, e);
                result.ThrowOnErrors();
            }
        }


        private void OnCatalogChanged(object sender, ComposablePartCatalogChangedEventArgs args)
        {
            IEnumerable<string> changedContractNames = args.ChangedParts
                    .SelectMany(part => part.ExportDefinitions)
                    .Select(export => export.ContractName).Distinct();

            this.OnExportsChanged(new ExportsChangedEventArgs(changedContractNames));
        }

        private ComposablePart GetComposablePart(ComposablePartDefinition partDefinition)
        {
            Assumes.IsTrue(partDefinition.Metadata.GetValue<bool>(MetadataServices.StandardKeys.IsFactoryKey) == false, "Shouldn't be calling this method if it is a factory");

            this.ThrowIfDisposed();
            this.EnsureRunning();

            ComposablePart part = null;
            using (new ReadLock(this._lock))
            {
                if (this._activatedSingletons.TryGetValue(partDefinition, out part))
                {
                    return part;
                }
            }

            // the part has not been created, we will create it outside the lock, assuming that partDefinition.CreatePart() is thread-safe
            ComposablePart newPart = partDefinition.CreatePart();

            // we will now write the part back into the dictionary, assuming someone else hasn't
            using (new WriteLock(this._lock))
            {
                if (!this._activatedSingletons.TryGetValue(partDefinition, out part))
                {
                    part = newPart;
                    this._activatedSingletons.Add(partDefinition, part);
                }
            }

            return part;
        }

        private Export CreateExportForFactory(ComposablePartDefinition partDefinition, ExportDefinition exportDefinition)
        {
            Assumes.IsFalse(this._isDisposed);

            IDictionary<string, object> exportMetadata = new Dictionary<string, object>(exportDefinition.Metadata);
            exportMetadata.SetValue<bool>(MetadataServices.StandardKeys.IsFactoryKey, true);

            return new Export(
                exportDefinition.ContractName,
                exportMetadata,
                () => this.GetExportedObject(() => partDefinition.CreatePart(), exportDefinition));
        }


        private Export CreateExportForSingleton(ComposablePartDefinition partDefinition, ExportDefinition exportDefinition)
        {
            Assumes.IsFalse(this._isDisposed);

            return new Export(
                exportDefinition,
                () => this.GetExportedObject(() => this.GetComposablePart(partDefinition), exportDefinition));

        }

        private object GetExportedObject(Func<ComposablePart> getComposablePart, ExportDefinition exportDefinition)
        {
            this.ThrowIfDisposed();
            this.EnsureRunning();

            ComposablePart part = getComposablePart();
            CompositionResult result = this._compositionEngine.SatisfyImportsAndRegisterForRecomposition(part);
            result.ThrowOnErrors();

            return part.GetExportedObject(exportDefinition);
        }

        [DebuggerStepThrough]
        private void ThrowIfDisposed()
        {
            if (this._isDisposed)
            {
                throw new ObjectDisposedException(this.GetType().Name);
            }
        }

        [DebuggerStepThrough]
        private void EnsureCanRun()
        {
            if ((this._sourceProvider == null) || (this._compositionEngine == null))
            {
                throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Strings.ObjectMustBeInitialized, "SourceProvider")); // NOLOC
            }
        }

        [DebuggerStepThrough]
        private void EnsureRunning()
        {
            if (!this._isRunning)
            {
                using (new WriteLock(this._lock))
                {
                    if (!this._isRunning)
                    {
                        this.EnsureCanRun();
                        this._isRunning = true;
                    }
                }
            }
        }

        [DebuggerStepThrough]
        private void EnsureCanSet<T>(T currentValue)
            where T : class
        {
            if ((this._isRunning) || (currentValue != null))
            {
                throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Strings.ObjectAlreadyInitialized));
            }
        }
    }
}
